6.15 Additional Capabilities of the ApplicationContext

正如本章介绍中所讨论的,org.springframework.beans.factory包提供了用于管理和操作bean的基本功能,包括以编程的方式。org.springframework.context包添加了扩展了BeanFactory接口的ApplicationContext接口,并扩展了其他接口,以更多的面向应用程序框架的风格提供附加功能。很多人完全以声明的方式使用ApplicationContext,不用编程方式创建它,而是依赖支持类(如ContextLoader)自动实例化ApplicationContext,作为Java EE Web应用程序的正常启动过程的一部分。 为了以更加面向框架的风格增强BeanFactory功能,context包还提供了以下功能: - 通过MessageSource接口访问i18n-style的消息 - 通过ResourceLoader接口访问资源,如URL和文件。 - 通过使用ApplicationEventPublisher接口,事件发布给实现ApplicationListener接口的bean。 - 通过使用HierarchicalBeanFactory接口加载多个(层级)上下文,允许每个上下文关注在一个特定层上,例如应用程序的Web层。

6.15.1 Internationalization using MessageSource

ApplicationContext接口扩展了一个名为MessageSource的接口,因此提供了国际化(i18n)功能。Spring还提供了接口HierarchicalMessageSource,它可以分层解析消息。 这些接口一起为Spring解析特定的消息提供了基础。这些接口定义了如下的方法: - String getMessage(String code,Object [] args,String default,Locale loc):用于从MessageSource中检索消息的基本方法。 当找不到指定语言环境的消息时,将使用默认消息。 使用标准库提供的MessageFormat功能,传入的任何参数都将成为替换值。 - String getMessage(String code, Object[] args, Locale loc): 基本上与前面的方法相同,但有一点不同:不能指定默认消息; 如果不能找到消息,则抛出NoSuchMessageException。 - String getMessage(MessageSourceResolvable resolvable,Locale locale):如果上述方法中使用的所有属性也都包含在名为MessageSourceResolvable的类中,您也可以使用此方法。 当一个ApplicationCOntext被加载时,它会自动在上下文中搜索定义的MessageSource的bean。这个bean的名字必须为messageSource。如果找到了这个bean,所有上述的方法在调用时会被委托给这个bean。如果找不到,ApplicationContext会尝试在上一级的容器中继续查找这个bean。如果找到了,则被作为Message Source的bean。如果ApplicationContext找不到任何Message Source,则会实例化一个空的DelegatingMessageSource,以便能够接受对上面定义的方法的调用。 Spring提供了两个MessageSource实现,ResourceBundleMessageSourceStaticMessageSource。 两者都实现了HierarchicalMessageSource以进行嵌套消息传递。StaticMessageSource很少使用,但提供了编程方式来添加消息到源。 以下示例中显示了ResourceBundleMessageSource

<beans>
    <bean id="messageSource"
            class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>format</value>
                <value>exceptions</value>
                <value>windows</value>
            </list>
        </property>
    </bean>
</beans>

在这个例子中,假设你在你的classpath中定义了三个资源包,分别叫做format,exceptions和windows。 任何解析消息的请求都将以JDK标准方式通过ResourceBundles解析消息。 为了演示这个例子,假设上述两个资源包文件的内容是...

# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.

下一个示例中显示了执行MessageSource功能的程序。 请记住,所有ApplicationContext实现也是MessageSource实现,因此可以转换为MessageSource接口。

public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("message", null, "Default", null);
    System.out.println(message);
}

上述程序的输出结果将会是...
Alligators rock!
所以总结一下,MessageSource被定义在一个名为beans.xml的文件中,该文件存在于你的classpath的根目录下。 messageSource bean定义通过其基本名称属性来引用许多资源包。 在列表中传递给basenames属性的三个文件作为文件存在于类路径的根目录,分别称为format.propertiesexceptions.propertieswindows.properties
下一个示例显示传递给消息查找的参数; 这些参数将被转换为字符串并插入到查找消息中的占位符中。

<beans>

    <!-- this MessageSource is being used in a web application -->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="exceptions"/>
    </bean>

    <!-- lets inject the above MessageSource into this POJO -->
    <bean id="example" class="com.foo.Example">
        <property name="messages" ref="messageSource"/>
    </bean>

</beans>
public class Example {

    private MessageSource messages;

    public void setMessages(MessageSource messages) {
        this.messages = messages;
    }

    public void execute() {
        String message = this.messages.getMessage("argument.required",
            new Object [] {"userDao"}, "Required", null);
        System.out.println(message);
    }
}

调用execute()方法后输出的结果是...
The userDao argument is required.
关于国际化(i18n),Spring各种的MessageSource的实现遵循与标准JDK的ResourceBundle相同的语言环境解析和回调规则。
简而言之,继续前面定义的messageSource示例,如果要解析英国(en-GB)语言环境的消息,则需要分别创建名为format_en_GB.propertiesexceptions_en_GB.propertieswindows_en_GB.properties的文件。 通常,区域解析是由应用程序的周围环境管理。在这个例子中,被解析的消息将被手动指定区域(英国)。

# in exceptions_en_GB.properties
argument.required=Ebagum lad, the {0} argument is required, I say, required.
public static void main(final String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("argument.required",
        new Object [] {"userDao"}, "Required", Locale.UK);
    System.out.println(message);
}

上述程序的输出结果将是...
Ebagum lad, the 'userDao' argument is required, I say, required.
您还可以使用MessageSourceAware接口来获取对已定义的任何MessageSource的引用。 任何实现MessageSourceAware接口的的bean在ApplicationContext创建和配置bean时都会被注入应用程序上下文中的MessageSource。

作为ResourceBundleMessageSource的替代方法,Spring提供了一个ReloadableResourceBundleMessageSource类。 这个变体支持相同的bundle文件格式,但比标准的基于JDK的ResourceBundleMessageSource实现更灵活。 特别是,它允许从任何Spring资源位置(而不仅仅是从类路径)读取文件,并支持热重载bundle属性文件(同时高效地缓存它们之间)。 查看ReloadableResourceBundleMessageSource javadoc获取详细信息。

6.15.2 Standard and Custom Events

ApplicationContext中的事件处理是通过ApplicationEvent类和ApplicationListener接口提供的。 如果一个实现ApplicationListener接口的bean被部署到上下文中,则每当ApplicationEvent发布到ApplicationContext时,都会通知该bean。本质上,这是标准的观察者模式。

从Spring 4.2开始,事件基础架构得到了显着的改进,并提供了基于注释的模型以及发布任意事件的能力,也就是说不一定非要从ApplicationEvent扩展的对象。 当这样的对象被发布时,我们把它包装在一个事件中。
Spring提供了以下的标准事件:
表6.7 内置事件

事件 说明
ContextRefreshedEvent ApplicationContext初始化或刷新时发布,例如,使用ConfigurableApplicationContext接口上的refresh()方法。 这里的“初始化”意味着所有的bean都被加载,检测并激活后处理器bean,单例被预先实例化,并且ApplicationContext对象已经可以使用了。 只要上下文没有关闭,只要所选的ApplicationContext实际上支持这种“热”刷新,刷新可以被触发多次。 例如,XmlWebApplicationContext支持热刷新,但GenericApplicationContext不支持。
ContextStartedEvent ApplicationContext启动时发布,使用ConfigurableApplicationContext接口上的start()方法时。 这里的“开始”意味着所有的生命周期bean都会收到明确的启动信号。 通常,这个信号用于在显式停止后重新启动Bean,但也可以用于启动尚未配置为自动启动的组件,例如尚未启动的组件。
ContextStoppedEvent ApplicationContext停止时发布,使用ConfigurableApplicationContext接口上的stop()方法时。 这里“停止”意味着所有生命周期的bean都会收到明确的停止信号。 停止的上下文可以通过start()调用重新启动。
ContextClosedEvent ApplicationContext关闭时发布,在ConfigurableApplicationContext接口上使用close()方法时。 这里的“关闭”意味着所有的单例bean被销毁。 一个关闭的上下文到达其生命的尽头; 它不能刷新或重新启动。
RequestHandledEvent 一个Web特定的事件,告诉所有的bean一个HTTP请求已被处理。 此事件在请求完成后发布。 此事件仅适用于使用Spring的DispatcherServlet的Web应用程序。

您也可以创建和发布自己的自定义事件。 这个例子用一个继承自Spring的ApplicationEvent基类的简单类做了演示:

public class BlackListEvent extends ApplicationEvent {

    private final String address;
    private final String test;

    public BlackListEvent(Object source, String address, String test) {
        super(source);
        this.address = address;
        this.test = test;
    }

    // accessor and other methods...
}

要发布自定义ApplicationEvent,需要通过ApplicationEventPublisher调用publishEvent()方法。 通常这是通过创建一个实现了ApplicationEventPublisherAware的类并将其注册为一个Spring bean来完成的。 下面的例子演示了这样一个类:

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blackList;
    private ApplicationEventPublisher publisher;

    public void setBlackList(List<String> blackList) {
        this.blackList = blackList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String text) {
        if (blackList.contains(address)) {
            BlackListEvent event = new BlackListEvent(this, address, text);
            publisher.publishEvent(event);
            return;
        }
        // send email...
    }
}

在配置的时候,Spring容器会检测到EmailService实现了ApplicationEventPublisherAware,并且会自动调用setApplicationEventPublisher()。 实际上,传入的参数将是Spring容器本身; 您只需通过ApplicationEventPublisher接口与应用程序上下文交互即可。
为了接收自定义的ApplicationEvent,要创建一个实现了ApplicationListener的类,并将其注册为一个Spring bean。 下面的例子演示了这样一个类:

public class BlackListNotifier implements ApplicationListener<BlackListEvent> {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlackListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

注意,ApplicationListener是根据自定义事件(BlackListEvent)的类型来确定泛型的。 这意味着onApplicationEvent()方法可以保持类型安全,避免任何的向下转换。您可以根据需要注册多个事件侦听器,但请注意,默认情况下,事件侦听器将同步接收事件。这意味着publishEvent()方法将阻塞,直到所有监听器完成处理事件。 这种同步和单线程方法的一个优点是,当侦听器接收到事件时,如果事务上下文可用,则它在发布者的事务上下文内运行。 如果需要另一个事件发布策略,请参考Spring的ApplicationEventMulticaster接口的javadoc。
以下示例显示了用于注册和配置上述每个类的bean定义:

<bean id="emailService" class="example.EmailService">
    <property name="blackList">
        <list>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
        </list>
    </property>
</bean>

<bean id="blackListNotifier" class="example.BlackListNotifier">
    <property name="notificationAddress" value="[email protected]"/>
</bean>

综合起来,当调用emailService bean的sendEmail()方法时,如果有任何应该列入黑名单的电子邮件,则会发布BlackListEvent类型的自定义事件。 blackListNotifier bean被注册为一个ApplicationListener,并且因此接收到BlackListEvent,然后它可以通知适当的人。

Spring的事件机制是为了在同一个应用程序上下文中的Spring bean之间进行简单的通信而设计的。但是,对于更复杂的企业集成需求,独立维护的Spring Integration项目提供了完整的支持,用众所周知的Spring编程模型构建轻量级,面向模式的事件驱动体系结构。

基于注释的事件监听器 从Spring 4.2开始,可以通过EventListener注解在bean的任何公共方法上注册一个事件监听器。 BlackListNotifier可以被重写如下:

public class BlackListNotifier {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    @EventListener
    public void processBlackListEvent(BlackListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

如上所见,方法签名再次声明它监听的事件类型,但是这次使用灵活的名称而不实现特定的监听器接口。 事件类型也可以通过泛型来缩小,只要实际事件类型在其实现层次结构中解析泛型参数即可。
如果您的方法应该监听多个事件,或者如果您定义的方法根本没有参数,那么也可以在注释本身上指定事件类型:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    ...
}

也可以通过注释的condition属性添加额外的运行时过滤,该属性定义了一个SpEL表达式,该表达式应匹配以实际调用方法时的特定事件。
例如,我们的通知器可以被重写为仅在事件的测试属性等于foo被调用:

@EventListener(condition = "#blEvent.test == 'foo'")
public void processBlackListEvent(BlackListEvent blEvent) {
    // notify appropriate parties via notificationAddress...
}

每个SpEL表达式都会重新评估一个专用的上下文。 下表列出了可用于上下文的项目,以便可以将它们用于条件事件处理:

表6.8. Event SpEL 可用的元数据 名称 | 位置 | 描述| 例子 ---|--- | --- | --- 事件| 根对象 | 实际的ApplicationEvent|#root.event 参数数组| 根对象 | 被调用的参数(如数组) | #root.args[0] 参数名称 | 执行上下文 | 任何方法参数的名称。 如果由于某些原因名称不可用(例如,没有调试信息),则参数名称也可在#a <#arg>下使用,其中#arg代表参数索引(从0开始)。|#blEvent or #a0 (one can also use #p0 or #p<#arg> notation as an alias).

请注意,#root.event只允许您访问基础事件,即使您的方法签名实际上是指发布的任意对象。
如果您需要发布一个事件作为处理另一个事件的结果,只需更改方法签名,返回应该发布的事件,如:

@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}

这个特性不支持异步的事件监听器

这个新方法将为每个由上述方法处理的BlackListEvent发布一个新的ListUpdateEvent事件。 如果您需要发布多个事件,则只需返回一组事件。

异步监听器
如果您希望可以异步处理事件特定的侦听器,只需重用常规的@Async支持即可:

@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
    // BlackListEvent is processed in a separate thread
}

使用异步事件时请注意以下几点限制: 1. 如果事件监听器抛出异常,它将不会传播给调用者,请查看AsyncUncaughtExceptionHandler以获取更多详细信息。 2. 这样的事件监听器不能发送回复。 如果您需要发送另一个事件作为处理的结果,注入ApplicationEventPublisher手动发送事件。

顺序监听器 如果你希望在调用该监听器之前先调用另一个监听器,只需要将@Order注释添加到方法声明中:

@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress...
}

泛型事件 您也可以使用泛型来进一步定义事件的结构。 你可以考虑一个EntityCreatedEvent <T>,其中T是实际创建的实体类型。 然后您可以创建以下监听器定义仅接收Person的EntityCreatedEvent

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    ...
}

由于泛型擦除,上面的方法只会当事件匹配了泛型的时候才能执行,其他的类型则会过滤掉(比如像class PersonCreatedEvent extends EntityCreatedEvent<Person> { … })。
在某些情况下,如果都要遵循某种结构(上述的情况)会变得很繁琐。这种情况下,你可以通过实现ResolvableTypeProvider帮助框架在运行时确定类型:

public class EntityCreatedEvent<T>
        extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(),
                ResolvableType.forInstance(getSource()));
    }
}

注意,这不仅对继承自PublishEvent的事件有效,也对作为事件发布的任何对象都是有效的。

6.15.3 Low-level资源的便捷访问

为了最佳的使用和理解应用程序上下文,用户通常应该熟悉Spring的Resource的抽象,如Chapter 8, Resources一章所述。
Application context是一个ResourceLoader,被用来加载Resources。一个Resource本质上是一个功能更加丰富的JDK中的java.net.URL类。事实上,Resource的实现就是对java.net.URL实例的包装。Resource可以以透明的方式从几乎任何位置获取低级资源,包括类路径,文件系统位置,任何可用标准URL描述的地方以及其他一些变体。如果资源位置字符串是一个没有任何特殊前缀的简单路径,那么这些资源就是来实际的应用上下文的合适的类型。
您可以为部署到应用程序上下文中的bean配置特殊的回调接口ResourceLoaderAware,该接口的方法会在初始化时被自动调用,并将application context 本身作为ResourceLoader传入。您还可以暴露出Resource类型的属性,用于访问静态资源; 他们将像其他任何属性一样被注入。您还可以将这些Resource属性指定为简单的String路径,并依靠上下文自动注册的特殊JavaBean PropertyEditor将这些文本字符串转换为实际的Resource对象。
ApplicationContext支持的路径结构就是资源字符串,对某些ApplicationContext的实现来说,这就是其中一种简单的方式。ClassPathXmlApplicationContext将简单的位置路径视为类路径位置。 您也可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或URL中加载定义,而不管实际的上下文类型如何。

6.15.4 Web应用中简便的ApplictionContext实例####

你可以通过使用ContextLoader来创建一个ApplicationContext。当然,开发者也可以通过编程的方式,通过ApplicationContext的实现来创建ApplicationContext。
您可以使用ContextLoaderListener来注册ApplicationContext,如下所示:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

监听器会检查contextConfigLocation的参数。如果这个参数不存在,监听器会使用 /WEB-INF/applicationContext.xml作为默认值。当参数确实存在时,监听器使用预定义的分隔符(逗号,分号和空格)分隔字符串,并将这些值用作应用程序上下文将被搜索的位置。同时,ANT-风格的路径格式也是被支持的。比如:/WEB-INF/*Context.xml,表示在“WEB-INF”目录下名称以“Context.xml”结尾的所有文件,/WEB-INF/**/*Context.xml表示在“WEB-INF”的任何目录下的以“Context.xml”结尾的文件。

6.15.5将Spring ApplicationContext作为Java EE的RAR文件部署####

可以将Spring ApplicationContext部署为RAR文件,将上下文及其所有必需的bean类和库JAR封装在Java EE RAR部署单元中。这相当于是在Java EE环境中独立启动了ApplicationContext,让ApplicationContext获得了访问J2EE服务器资源的能力。比起部署WAR包,RAR是更为自然的选择,实际上,不含HTTP访问的WAR包仅仅是用来在J2EE环境中独立启动ApplicationContext。
对不需要HTTP的任务来说(比如定时任务),部署RAR文件更为合适。Context中的Bean可以使用应用程序服务器资源,例如JTA事务管理器和JNDI绑定的JDBC DataSources和得到JMS ConnectionFactory实例,或者注册平台的JMX服务器。这些都可以通过Spring的标准事务管理和JMX的支持实现。应用程序组件还可以通过Spring的TaskExecutor抽象与应用程序服务器的JCA WorkManager进行交互。
查看SpringContextResourceAdapter的文档,了解RAR部署中涉及的配置细节。
要将Spring ApplicationContext简单部署为Java EE RAR文件,请将所有应用程序类打包到RAR文件中,RAR是标准JAR文件,只是名称不同。 将依赖的JAR包也添加到RAR文件的根目录中。添加一个“META-INF / ra.xml”部署描述文件(如SpringContextResourceAdapter的 javadoc所示)和相应的Spring XML bean定义文件(通常为“META-INF / applicationContext.xml”),并将生成的RAR文件放到您的应用程序服务器的部署目录。

这种RAR部署文件通常是独立的; 它们不会将组件暴露给外界,甚至不会暴露给同一应用程序的其他模块。与基于RAR的ApplicationContext的交互通常通过与其他模块共享JMS。基于RAR的ApplicationContext也可以做一些调度作业,例如,处理文件系统中的新文件(或诸如此类)。如果需要从外部进行同步的访问,可以通过RMI服务实现。当然,同一服务的不同模块也可以这么做。